京东数科七层负载 | HTTPS硬件加速 (Freescale加速卡篇)
作者:庞成
京东数科基础云平台团队原创,转载请获得授权
京东数科JDDLB作为京东数科最重要的公网流量入口,承接了很多重要业务的公网流量。目前,已完全接替商业设备F5承载所有的流量,并在数次618、11.11大促中体现出优越的功能、性能优势。
京东数科JDDLB 整体架构
图1 京东数科JDDLB 整体架构
JDDLB 整体架构的核心包括:基于DPDK自主研发的四层负载SLB,定制开发扩展功能的NGINX,以及统一管控运维平台。其主要特点为:
高性能:具备千万级并发和百万级新建能力。
高可用:通过 ECMP、会话同步、健康检查等,提供由负载本身至业务服务器多层次的高可用。
可拓展:支持四层/七层负载集群、业务服务器的横向弹性伸缩、灰度发布。
四层负载能力:通过ospf 向交换机宣告vip;支持ECMP、session 同步;支持均衡算法如轮询、加权轮询、加权最小连接数、优先级、一致性哈希;FullNAT转发模式方便部署等。
七层负载能力:支持基于域名和URL的转发规则配置;支持均衡算法如轮询、基于源 IP 哈希、基于cookie等。
SSL/TLS能力:证书、私钥、握手策略的管理配置;支持 SNI 配置;支持基于多种加速卡的SSL卸载硬件加速等。
流量防控:提供一定的 Syn-Flood 防护能力;与应用防火墙结合后提供 WAF 防护能力;提供网络流量控制手段如 Qos 流控、ACL 访问控制等。
管控平台:支持多种维度的网络和业务指标监控和告警。
优化方案性能提升对比
使用单加速卡,平均新建速率提升130%,且CPU利用率降低40%。 使用双加速卡,平均新建速率提升320%。
表1 性能提升对比
使用WRK性能压测工具作为HTTPSClient,在同一物理机上部署NGINX和加速卡,分别测试软件加解密、单加速卡卸载、双加速卡卸载场景下HTTPS平均新建速率:
使用OpenSSL软件加解密,不断增加WRK数量,直到CPU-idle接近于0%,CPU无法及时处理新连接请求,此时CPU达到瓶颈,可认为软件加解密的最大新建能力。
使用单加速卡卸载,不断增加WRK数量,直到出现硬件请求队列入队失败,此时可认为使用单加速卡正常工作时的新建能力,避免入队失败走软解导致CPU性能快速消耗。使用加速卡时,加解密请求入队失败后会进行软件加解密,不影响SSL/TLS协议处理。
使用双加速卡卸载,不断增加WRK数量,直到CPU-idle接近于0%,CPU无法及时处理新连接请求,此时CPU已到达瓶颈,并不是双加速卡的最大性能。
采用定制开发的PCI加速卡,每个PCI卡上集成3~4个Freescale C291处理器,作为主CPU外的协处理器,处理耗时的加解密等计算。 驱动支持多加速卡同时进行卸载加速。 定制开发FSL Engine,通过OpenSSL Engine机制将FSL 硬件加解密计算能力集成到OpenSSL加密库中。 NGINX采用异步模式调用OpenSSL API,代替传统的同步模式调用。
SSL/TLS基本概念
HTTPS简单理解成HTTP over SSL/TLS。客户端和服务端在使用HTTPS传输业务数据前,首先由SSL/TLS协议在两端之间建立安全信道(这个过程称作握手协商阶段),然后在该安全信道上对HTTP业务报文进行加密传输(这个过程称作加密数据传输阶段)。SSL/TLS的安全性体现在哪里,解决了哪些安全问题,如何解决的,下面一步步介绍。这里,先了解下客户端和服务端之间使用明文HTTP通信存在的安全问题。
图3 HTTP安全问题及HTTP到HTTPS的改造
数据保密性:保证数据内容在传输的过程中不会被第三方查看,防止用户数据信息的泄漏。 数据完整性:及时发现被第三方篡改的传输内容,一旦发现数据被篡改过则拒绝接收。 身份校验安全性:保证数据到达用户期望的目的地。客户端需要验证目前正在通信的对端是否为期望的服务器,而非假冒的服务器。反之,服务器也可以对客户端进行有效性验证。
1、数据保密性
图4 数据保密性实现流程
数据保密性需要通信双方具有相同的密钥,而且这个密钥只能通信双方知道,不能被第三方获取。实际通信中,这个密钥并不是固定不变的,也不会保存到磁盘文件中。客户端每次和服务器建立新连接的时候,都会重新协商出相同的密钥。在SSL/TLS协议的第一阶段——握手协商阶段,服务器和客户端会交互一些报文信息,服务器和客户端根据报文中的信息各自生成相同的密钥,并把密钥保存在内存中。一旦这个连接断开,内存中的密钥将会自动销毁,避免密钥的泄漏。
2、数据完整性
数据完整性用于防止HTTP数据被篡改,如果一旦发现数据被篡改则拒收数据包。使用的算法称作消息验证码算法MAC。数据完整性实现流程如下图:
图5 数据完整性实现流程
和数据保密性中的密钥获取方式一样,这里的密钥也是在SSL/TLS密钥协商阶段生成的、相同的、并保存在各自内存中。
3、身份校验安全性
HTTPS中,客户端需要对通信对端的身份有效性进行校验,确定客户端是和期望的真正服务端通信,而非和冒充的攻击者进行通信。身份校验安全性主要利用数字证书技术实现。数字证书涉及的概念非常多,比如数字证书签发、CA证书、根证书、证书链、证书有效性校验、非对称密钥算法、签名/验证等,本文不做全面介绍,仅描述SSL/TLS性能优化涉及的概念:
对称密钥算法中,加解密操作使用的密钥是同一个,且通信双方都需要知道这个密钥。而非对称密钥算法有两个密钥,组成一个密钥对;可以公开的密钥为公钥,公钥谁都可以知道,不怕泄漏;需要保密不能泄露的密钥称为私钥,私钥只有自己知道,不能被泄漏;通信双方的每一方,可以把自己的公钥发送给对端,但自己的私钥一定只有自己知道;同一份数据,使用公钥加密,私钥可以解密,反之,私钥加密,公钥可以解密。
图6 非对称密钥用法一——通信数据加密传输
如图6所示,在SSL/TLS协议中,存在使用非对称密钥算法对通信数据进行加密传输的操作,此种场景下将使用图6中左图的流程。这里需要注意图6右图流程,公钥是公开的,谁都可以知道,用私钥加密的数据是不具有保密性的,因为只要拥有公钥就能解密数据,所有窃听者都可以取到公钥,都会看到真实数据内容。实际上,图6右图“私钥加密-公钥解密”的使用场景称作签名的生成和校验,SSL/TLS协议也会用到,签名生成和验证流程如下图:
图7 非对称密钥应用二——通信数据签名的生成及验证
私钥加密的数据——签名值——不具备保密性,但却有校验作用。图7中,通信数据是明文消息加上该消息的签名值,签名值用于对明文消息进行校验,验证明文消息的正确性、是否被篡改过。
非对称密钥算法的用法,总结下来就是:
公钥加密-私钥解密→用于数据加密传输→通信数据是密文
私钥加密-公钥解密→用于签名的生成和校验→通信数据是明文消息加上消息签名值
(2)数字证书
数字证书的签发、数字证书的校验、证书链的相关细节,本文不做详述,只要知道以下几点:
① HTTP到HTTPS,客户端和服务器涉及的一些改造,主要包括
服务端:1)服务端需要针对某个服务配置SSL/TLS相关配置项,如密码套件、证书/私钥文件路径。2)部署由CA机构签发的,包含服务器公钥、服务器域名信息的数字证书,可公开证书。3)部署和公钥配对的私钥文件,私钥文件要具有一定安全保护措施,不能被泄漏。
客户端:浏览器配置CA证书、根证书等文件,一般默认内置。
② 证书的使用主要在SSL/TLS协议握手协商阶段,简述如下:
客户端和服务端建连后,在SSL/TLS协议握手协商阶段初期,服务端将自己的数字证书发送给客户端,服务端私钥文件自己使用。
客户端收到服务器数字证书后,通过配置的CA证书、根证书,依据证书链的校验逻辑,验证服务器数字证书的有效性,一旦验证通过,说明服务器身份正确。
客户端从数字证书中取出服务器公钥,服务端从自己的私钥文件中取出服务器私钥。
之后,客户端和服务端就会使用这对非对称密钥进行SSL/TLS协议握手协商阶段的其他处理。RSA密钥协商算法中使用了图6的用法,而DHE、ECDHE密钥协商算法中使用了图7的用法。
SSL/TLS的性能问题
图8 使用RSA密钥协商算法时的协议流程图
1、SSL/TLS协议的两个处理阶段
为了能够明确SSL/TLS协议的哪个环节存在性能问题,以及加速卡是在哪一步发挥作用的,需要介绍下SSL/TLS协议的两个处理阶段。
① TLS/SSL协议,握手协商阶段,主要做的事情:
服务器身份验证:服务器会将自己的数字证书发送给客户端,客户端使用保存在本地的CA证书、根证书验证服务器数字证书的有效性,从而确认服务器身份。
密钥协商:客户端和服务器之间,需要使用对称加密算法对业务数据进行加密/解密,做到通信数据的保密性;使用消息验证码算法对业务数据进行MAC值计算和校验,检测出通信数据是否被篡改。这要求通信双方必须使用相同的对称加密算法及密钥,使用相同的消息验证码算法及密钥;密钥相同才能正确加解密,才能正确进行MAC计算/校验。
密钥协商,就是在客户端和服务器之间协商出一模一样的对称加密算法密钥和MAC密钥,而且这些密钥还不能被第三方知道。如前所述,这些密钥每次连接需要重新协商,并保存在内存中,连接断开则销毁。
② TLS/SSL协议,加密数据传输阶段,主要做的事情:
这个阶段就是业务数据交互的阶段,针对每个业务报文都需要使用协商出的对称密钥进行加解密,使用协商出的MAC密钥进行MAC值计算和校验。
2、SSL/TLS的性能问题
理论分析和实际测试证明,非对称密钥算法的计算是非常耗时、耗CPU计算资源的,而对称加密算法的计算速度比非对称密钥算法的计算速度快很多很多。JDDLB七层负载是基于NGINX实现的,NGINX支持多个进程同时运行处理业务,但每个进程只有一个线程。当进程执行某个连接上SSL/TLS协议握手协商阶段的非对称密钥计算时,会一直占用CPU计算资源,导致进程没有其他执行机会从监听套接字连接队列中取得新的连接请求,连接请求会在队列中积压,直到队列溢出丢弃新连接,造成进程新建连接速率不高。
所以,实际部署中,在七层负载每个服务器上部署FSL加速卡,将SSL/TLS协议中涉及的非常耗CPU计算资源的数学运算(主要是非对称密钥计算)卸载到FSL加速卡中,释放CPU资源来及时处理新的连接请求以及处理其他业务,提高整体负载的新建连接速率。
理论上,所有使用SSL/TLS的业务都可以借助于FSL加速卡的运算能力进行提速。
同步/异步模式及性能提升
FSL加速卡是一个PCI外设,和PCI网卡类似,网卡需要在内核中安装网卡驱动(用户态网卡驱动——如DPDK——不在本文讨论范围内)才能正常使用,FSL加速卡也需要编写内核驱动程序。
按照Linux中外设即文件的思想,外设驱动程序安装后,一般会在/dev目录下生成对应的设备文件,操作外设就是操作(比如open/close/read/write/ioctl等文件操作接口)对应的设备文件。FSL加速卡的使用也是通过操作设备文件的方式来完成的,对应的设备文件为/dev/crypto,支持的操作接口有open/close/ioctl,并且支持select/poll/epoll机制。用户态程序使用这些接口和内核态驱动程序交互,驱动程序按照接口中指定的命令操作加速卡。
如前所述,SSL/TLS性能问题的优化,就是将耗CPU计算资源的非对称密钥计算让加速卡来完成,释放CPU去干其他工作,这样CPU和加速卡可以并行处理。那么,很容易想到的是,当用户态进程在SSL/TLS协议报文处理过程中,如果有一个非对称密钥加密的密文数据需要解密,进程将操作命令、密文数据、公私密钥对信息等信息,通过ioctl()的方式交给驱动程序,由驱动程序交给加速卡硬件进行解密;然后,驱动程序从加速卡中取出解密结果,在返回给进程。其他非对称密钥的操作,如加解密、签名生成和校验、公私密钥对的生成都可以按照这个思路进行优化。
此处,遇到第一个同步/异步模式,就是驱动程序ioctl()接口的操作命令的设计:
同步操作命令:用户态进程调用ioctl(/dev/crypto,同步操作命令,其他参数)接口(其他参数,指的是待加密/解密数据、密钥对信息等),向加速卡驱动提交计算请求,进程阻塞在ioctl()系统调用上,直到驱动程序将计算结果返回给用户态后,ioctl()系统调用返回。之后,用户进程继续执行ioctl()后面的代码。
异步操作命令:用户态进程调用ioctl(/dev/crypto,异步操作命令,其他参数)接口,向加速卡驱动提交计算请求,请求成功提交给驱动后,ioctl()系统调用就会返回。用户进程不会阻塞等待计算结果,继续执行ioctl()后面的代码。
那么,用户态进程什么时候可以从驱动程序中取得计算结果呢?
用户态进程可以轮训执行ioctl(/dev/crypto,取结果命令,其他参数)接口,检查驱动程序中是否有完成的计算结果。如果存在完成的计算结果,则会将结果拷贝到用户态。
此外,驱动程序支持事件通知(select/poll/epoll)机制。用户态进程在调用ioctl()提交异步操作命令并返回后,进程执行select/poll/epoll操作,监听/dev/crypto设备文件的可读事件。当驱动程序从加速卡硬件获取计算结果后,驱动程序会触发/dev/crypto设备文件的可读事件。用户态进程会从select/poll/epoll调用中返回,接着执行ioctl(/dev/crypto,取结果命令,其他参数)接口,将结果拷贝到用户态。
驱动程序设计中支持同步/异步模式。
2、OpenSSL Engine
OpenSSL Engine机制,目的是为了使OpenSSL能够透明地使用第三方提供的软件加密库或者硬件加密设备进行加密。OpenSSLEngine机制使得OpenSSL已经不仅仅使一个加密库,而是提供了一个通用的加密接口,能够与绝大部分加密库或者加密设备协调工作。
FSL加速卡也是通过OpenSSL Engine机制将自己的硬件加密库结合到OpenSSL库中,使得NGINX进程在调用OpenSSL API时使用硬件进行加解密操作。
图9 软件栈
软件栈中,NGINX是服务器上运行的处理七层负载业务的用户进程,使用OpenSSL Lib支持SSL/TLS协议。OpenSSL Lib主要包含两部分,处理SSL/TLS协议报文的OpenSSL libssl库,和处理各种加解密计算/大数运算的OpenSSL libcrypto库。OpenSSL libcrypto库在进行加解密计算时,如果指定了Engine,将调用Engine提供的计算接口。FSL Driver是内核驱动程序,安装驱动程序后会创建/dev/crypto设备文件。FSL Engine提供了各种算法的计算接口(如RSA、ECDHE、DHE、ECDSA等算法)给上层调用,每个计算接口实现中都会使用ioctl(/dev/crypto, 命令,参数)方式,将加解密请求提交给驱动程序或者从驱动程序中取出计算结果;FSL Engine同时监听处理设备文件/dev/crypto的可读事件。NGINX开发一个模块,用来管理FSL Engine和FSL加速卡的一些配置。
NGINX支持多个worker进程同时运行处理业务,每个worker进程只有一个线程,该线程运行一个主循环。NGINX的高并发来源于其异步非阻塞的事件驱动,NGINX中的每个事件由ngx_event_t实例表示,每个ngx_event_t实例都有三个重要属性:超时时间、事件处理函数、对应某个文件描述符的可读事件或者可写事件。NGINX进程的主循环会监听一系列事件,如果某个事件发生了超时、或者对应的文件描述符可读/可写事件发生了,则调用事件处理函数;该事件处理函数执行完毕后,主循环会再次执行监听动作。worker进程主循环处理逻辑示意图如下所示:
图10 NGINX事件循环
NGINX进程会调用OpenSSL API函数SSL_do_handshake()来完成SSL/TLS协议握手协商阶段的所有工作,SSL_do_handshake()内部会维护一个SSL/TLS协议的状态机。由于SSL/TLS握手阶段涉及多次报文交互,SSL_do_handshake()函数会在多次事件处理函数中被调用。
下面,我们讨论OpenSSL API的同步/异步模式,并分析对SSL/TLS整体性能的影响:
(1)OpenSSL同步模式
图11 OpenSSL API同步模式使用FSL加速卡
上图是OpenSSL API同步模式下SSL_do_handshake()使用FSL加速卡的流程图:
① TCP连接上有SSL/TLS协议报文到达,连接套接字可读事件发生,调用事件处理函数ngx_ssl_handshake_handler()继续处理握手流程。
② SSL_do_handshake()处理协议报文,当发现收到的数据需要解密、或者要对发送数据进行加密时,调用OpenSSL Engine对应算法的计算接口函数。比如,RSA加解密使用的一种核心运算——大数模指数运算——在FSL Engine对应的实现接口就是cryptodev_rsa_mod_exp(),这个接口将会使用FSL加速卡来进行大数模指数运算。
③ cryptodev_rsa_mod_exp()函数中调用ioctl(/dev/crypto, 同步操作命令,其他参数)接口,将加解密请求提交给驱动程序。如前所述,这里的同步操作命令,将导致调用进程阻塞,一直等驱动程序将加解密结果返回给用户态后,ioctl()才返回。而这段时间,该进程将不能处理任何业务,CPU资源也会被系统调度给其他任务使用。
④ 加解密计算完成后,继续处理SSL/TLS协议的其他逻辑代码。最后,本次事件处理完毕,进程回到主循环,继续监听等待其他事件的发生。
(2)OpenSSL API异步模式
OpenSSL API异步模式是通过ASYNC_JOB机制实现的,本质上就是一套协程库。这里,以自己理解的OpenSSL ASYNC_JOB机制,对协程做一个简单介绍。协程(CoRoutine)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
对比一下线程概念:
一个进程可以有多个线程实例;
每个线程都有自己的栈空间,有自己的上下文环境;
每个线程在创建的时候会指定一个线程函数,线程运行的第一个函数;
线程函数使用的栈是线程自己的栈,多个线程的栈空间不同,所以,即使多个线程执行相同的函数,函数中的栈变量都是在各自线程栈空间里,多个线程同时运行互不干扰;
多个线程使用的堆空间还是进程的;
线程调度器是操作系统内核,操作系统负责维护线程的创建、销毁、调度、上下文切换等操作;
协程的很多概念和线程很像,可以对比着理解:
一个线程可以维护多个协程实例,这些协程都由这个线程创建、维护、调度;
每个协程都有自己的栈空间,有自己的上下文环境;
每个协程在创建的时候会指定一个协程函数,协程运行的第一个函数;
协程函数使用的栈是协程自己的栈,多个协程的栈空间不同,所以,即使多个协程执行相同的函数,函数中的栈变量都是在各自协程栈空间里,同一线程的多个协程先后运行(这里是先后运行,阅读后面内容后就会理解这一点)互不干扰。
多个协程使用的堆空间还是进程的;
协程调度器是线程,调度器可以理解成该线程执行的某个调度函数和该线程独享的调度有关的一组数据(比如,保存协程上下文和协程状态的数据结构,保存调度器信息的数据结构等)。协程调度器负责创建/回收/调度协程、协程上下文的切换、维护协程状态机。
图12 协程调度流程
协程有两个重要的原语:resume语义、yield语义:
① resume语义:由协程调度器使用,保存调度器当前上下文环境,让出CPU,切换到指定协程上下文。如果协程首次运行,则上下文为初始化上下文,协程将从协程函数开始执行;否则,上下文是协程上次执行yield语义时保存的上下文,协程将从上次yield位置之后继续运行。
② yield语义:由协程实例使用,保存协程当前上下文,让出CPU,切换回协程调度器上下文。调度器将从上次执行resume位置之后继续运行。
图12中描述了同一个线程上先后创建的两个协程的基本调度流程:
① 协程调度器首先创建协程1实例,指定协程1使用的栈空间,初始化协程上下文环境,协程函数为CoRoutine_Func(),初始化协程状态,设置协程运行的任务函数Task_Func_1()以及参数信息。之后使用resume语义切换上下文到协程运行,开始执行CoRoutine_Func()。
②CoRoutine_Func()中调用用户指定的任务函数Task_Func_1()。当Task_Func_1()函数内部执行系统调用时,一般采用非阻塞方式,比如Task_Func_1()函数内部需要读取套接字上的数据,则read()操作指定非阻塞标志位。这是因为,如果采用阻塞方式调用read(),套接字上没有可读数据,将使整个协程阻塞在read()系统调用上,也就是协程对应的线程阻塞在read()上,该线程上的其他协程都将无法被调度。如图12所示,Task_Func_1()函数执行一些任务后,发现需要满足一定事件条件后才能继续后面的处理(比如,非阻塞调用read()返回错误码为EAGAIN或者EWOULDBLOCK,而函数需要收到更多套接字数据,然后才能继续执行后续处理),此时协程会执行yield语义,并通过某种方式将协程1所需等待的事件告诉给调度器(线程)。yield语义将上下文切换回调度器。之后,或者调度器再次调度其他协程运行,或者调度器返回执行其他线程任务。
③ 类似①
④ 类似②
⑤ 在②中,协程1可以将自己需要等待的事件告诉给调度器(线程),比如,协程1将希望等待的套接字可读事件告诉调度器(线程)。当事件发生后,线程再次运行调度器,执行resume语义,切换到协程1上次执行yield的位置,继续执行协程1后面的任务。
⑥ 用户任务Task_Func_1()函数执行完毕后,会返回到CoRoutine_Func()函数中,进行一些协程的收尾工作,比如,设置协程的状态为结束状态、最终切换回调度器上次执行resume的位置等。这里,并不把用户的任务函数Task_Func_1()直接作为协程函数,这是因为,协程函数要管理协程的状态和收尾工作,一般封装好的协程库,协程函数也是封装好的,用户任务函数不用关心协程的状态和收尾工作,只要知道在任务函数中什么时候需要执行yield语义就好。
⑦ 类似⑤
⑧ 类似⑥
这里需要注意的是,一个线程内的多个协程虽然可以切换,但只能是调度器和协程之间可以相互切换,不能从一个协程上下文直接切换到另一个协程上下文。而且,同一个线程的多个协程是先后串行执行的,在内核看来就是一个线程在运行,所有协程的运行时间都是该线程的时间片,内核中并没有进行线程的切换;一个线程同一时间只能在一个CPU上运行,所以,没法利用CPU多核能力在多个CPU上同时运行一个线程内的多个协程。不同线程可以利用CPU多核同时运行,不同线程上的协程可以同时运行在不同CPU上。
OpenSSL ASYNC_JOB机制相关结构体和函数,可以对比上述协程概念进行理解:
图13 ASYNC_JOB机制相关结构体
OpenSSL ASYNC_JOB机制中,async_ctx保存调度器上下文和一些调度信息,该结构体实例是per thread的,每个线程都有自己的调度器。ASYNC_JOB就是协程,ASYNC_WAIT_CTX用于保存协程等待的事件fd;在协程内部执行yield语义前,将协程需要等待的事件fd保存在这个结构体中,并传递给线程;调度器函数返回到线程后,线程(NGINX)可以将该fd添加到epoll列表中;当NGINX主循环监听到这个fd的事件后,则再次执行调度器,resume到协程上次yield的位置继续执行。ASYNC_start_job()相当于协程调度器函数,负责协程的创建和初始化,并实现resume语义;初始化的协程函数是async_start_func()。ASYNC_pause_job实现yield语义。
在理解了OpenSSL异步模式的实现后,看一下OpenSSL异步模式下如何使用FSL加速卡:
图14 OpenSSL API异步模式使用FSL加速卡
① TCP连接上有SSL/TLS协议报文到达,TCP连接套接字可读事件发生,调用事件处理函数ngx_ssl_handshake_handler()继续处理SSL/TLS握手流程。
② SSL_do_handshake()在OpenSSL同步模式和异步模式下的执行流程有很大的不同。在异步模式下,SSL_do_handshake()主要工作是调用协程相关函数管理握手阶段的协程。调用ssl_start_async_job()运行协程,指定协程的用户任务函数为ssl_do_handshake_intern(),该任务函数完成整个SSL/TLS握手阶段的工作。就是说,所有SSL/TLS握手阶段处理都是在协程中完成。这里,创建用于存放协程ASYNC event fd的结构体ASYNC_WAIT_CTX。
③ 首次运行协程,ASYNC_start_job()函数会先创建协程并初始化,指定协程函数为async_start_func()。接着就切换到协程上下文执行协程函数async_start_func(),并调用握手处理函数ssl_do_handshake_intern()。
④ 当握手阶段需要进行非对称密钥计算时,将调用FSL Engine中提供的计算接口。这里的cryptodev_rsa_mod_exp()是FSL Engine定义的RSA算法中大数模指数运算接口函数。
⑤ 创建ASYNC event fd,并将fd保存在(2)中创建的ASYNC_WAIT_CTX结构体中,最终将该fd传递给线程,并加入到NGINX事件框架epoll中。该fd的可读事件表示协程等待事件发生,可以再次调度协程继续后面的处理。
⑥ 调用ioctl(/dev/crypto, 异步操作命令,其他参数)接口,将计算请求提交给加速卡驱动。这里使用的是加速卡驱动的异步操作命令,请求递交成功后立即返回,ioctl系统调用不会阻塞等待结果完成。
⑦ 执行ASYNC_pause_job()操作,协程暂停运行,让出CPU,等待计算结果完成后继续后面的处理。
⑧ 取出②、⑤步骤设置的ASYNC event fd,添加到NGINX事件框架epoll监听列表。
⑨ NGINX主循环再次阻塞监听事件。
⑩ 当加速卡完成计算后,通过某种方式触发ASYNC event fd的可读事件,NGINX事件框架监听到该事件后,再次调用事件处理函数继续处理握手操作。
⑪ 恢复协程上次暂停的位置继续执行后面操作。
⑫ 执行ioctl(/dev/crypto, 取结果命令,其他参数)接口,从驱动中取出加速卡的计算结果。
⑬ SSL/TLS整个握手阶段处理完成,协程结束,返回到调度器。
⑭ 握手阶段完成,将TCP连接套接字加入NGINX事件框架epoll监听列表,返回到NGINX事件框架继续监听套接字事件。此后的事件处理,都是SSL/TLS加密数据传输阶段。
步骤②、⑤、⑧中涉及的ASYNC event fd是由eventfd()系统调用生成的事件文件描述符,是操作系统的一种事件通知机制。ASYNCevent fd并不是驱动设备文件/dev/crypto。实际编码实现中,当监听/dev/crypto的可读事件(该可读事件表示加速卡有可取的计算结果)发生时,会触发ASYNCevent fd的可读事件。图14展示的流程图基本描述了OpenSSLAPI异步模式下如何使用FSL加速卡,和实际编码实现稍有些差异。
(3)OpenSSL API同步/异步模式总结
上面描述了OpenSSL API同步模式和异步模式下FSL加速卡的使用情况。
同步模式下,如图11,进程在将非对称密钥计算请求提交给驱动后,进程是阻塞等待计算结果的。虽然此时的CPU会被系统调度给其他任务使用,但是当前进程无法执行其他业务。进程无法及时处理监听套接字队列中的新连接,也会有连接积压而丢失情况,影响进程的新建连接速率。
异步模式下,如图14,进程在将非对称密钥计算请求提交给驱动后,进程并没有阻塞等待计算结果。进程继续处理其他业务,及时处理新的连接请求。由此可以看出,异步模式可以更好的提高新建连接速率。
FSL加速卡驱动概览
图15 加速卡驱动架构
本优化方案中Freescale C291处理器,作为主CPU外的协处理器,处理非对称密钥的计算。服务器安装FSL加速卡后,服务器启动时进行的PCI设备枚举过程,会将加速卡上的每一个C291处理器识别为一个单独的PCI设备。加速卡驱动程序主要组成有:
1、fsl_pkc_crypto_offload_drv.ko C291处理器驱动内核模块
fsl_pkc_crypto_offload_drv.ko内核模块是FSL C291处理器的驱动程序,运行于主CPU之上。与该内核模块对应的,运行于C291协处理器上的固件程序是/etc/crypto/pkc-firmware.bin。这两个软件模块之间,通过主机物理内存和C291协处理器内部内存(映射为主机的PCI BAR空间)进行数据交换,交换加解密请求和响应数据,交互过程涉及DMA操作。
fsl_pkc_crypto_offload_drv.ko内核模块管理的request queue中的每个请求,包含请求数据内容、处理该请求对应响应的回调函数及回调函数参数,每一个请求中都指定了如何处理该请求的响应。当fsl_pkc_crypto_offload_drv.ko内核模块从C291协处理器获取响应后,就调用回调函数来处理响应。这么做的原因是,fsl_pkc_crypto_offload_drv.ko内核模块无法感知请求是内核中的哪个组件添加的,所以,内核组件在添加请求的同时,由组件指定处理请求的回调函数。
fsl_pkc_crypto_offload_drv.ko内核模块会向内核中注册一个高优先级工作队列。当C291处理器有完成的计算结果时,通过中断通知主CPU。主CPU运行中断处理程序,中断处理程序触发高优先级工作队列运行。工作队列获取响应,该响应对应的请求中记录了处理该响应的回调函数及参数,工作队列将调用回调函数进一步处理这个响应。这里采用高优先级工作队列,保证响应能被及时处理,防止响应积压导致新的请求无法入队。
fsl_pkc_crypto_offload_drv.ko内核模块会向内核Linux Crypto子系统中注册驱动支持的算法以及算法处理函数。内核中的其他组件都可以通过Linux Crypto子系统API使用FSL加速卡。一般情况下,内核中的module_x模块通过Linux Crypto子系统API向驱动request queue中添加需要进行加解密操作的请求,同时指定处理该请求对应响应的回调函数及参数,这个回调函数及参数是module_x中定义的。当驱动取得响应后,工作队列将按照module_x定义的回调函数取处理响应。
Linux Crypto子系统API在使用FSL加速卡时,采用轮训方式使用每一个C291处理器。所以,本优化方案支持多个加速卡同时使用,提高并发处理能,提供更好的性能优化。
2、cryptodev.ko 设备文件驱动程序
cryptodev.ko内核模块是/dev/crypto字符设备文件驱动程序,处理字符设备文件的open()、close()、ioctl()等系统调用,支持epoll()/select()/poll()机制。可以理解为用户态进程 和fsl_pkc_crypto_offload_drv.ko之间的接口层,将用户态加解密数据进行转换以适合驱动程序使用,同时处理响应队列。
cryptodev.ko内核模块接收用户态ioctl()传递过来的请求,按照fsl_pkc_crypto_offload_drv.ko
要求的请求/响应格式组织数据,同时分配fsl_pkc_crypto_offload_drv.ko请求/响应需要的DMA内存。
cryptodev.ko通过LinuxCrypto子系统API向 fsl_pkc_crypto_offload_drv.ko添加请求,同时指定请求的回调函数及函数参数。回调函数是cryptodev.ko中定义的,会把响应挂在cryptodev.ko模块指定的响应链表中。当响应链表中存在响应时,cryptodev.ko可触发/dev/crypto可读事件通知用户态进程。之后,用户进程从这个链表中取响应到用户态。
每次执行dev_fd = open(/dev/crypto)操作,cryptodev.ko内核模块都会为dev_fd单独分配资源,比如,单独分配响应链表等。这样,多个NGINX进程同时使用/dev/crypto设备文件时,每个进程都执行open()操作,分配各个进程自己对应的资源,可以做到各个进程不冲突。
相关阅读
关注技术说,我们只凭技术说话!